/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.naming.resources;

import java.io.File;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.naming.NamingException;
import javax.naming.directory.Attributes;

import org.apache.naming.NamingEntry;

/**
 * Extended FileDirContext implementation that will allow loading of tld files
 * from the META-INF directory (or subdirectories) in classpath. This will fully
 * mimic the behavior of compressed jars also when using unjarred resources. Tld
 * files can be loaded indifferently from WEB-INF webapp dir (or subdirs) or
 * from META-INF dir from jars available in the classpath: using this DirContext
 * implementation you will be able to use unexpanded jars during development and
 * to make any tld in them virtually available to the webapp.
 * 
 * Sample context xml configuration:
 * 
 * <code>
 * &lt;Context docBase="\webapps\mydocbase">
 *   &lt;Resources className="org.apache.naming.resources.VirtualDirContext"
 *              virtualClasspath="\dir\classes;\somedir\somejar.jar"/>
 * &lt;/Resources>
 * </code>
 * 
 * 
 * <strong>This is not meant to be used for production. Its meant to ease
 * development with IDE's without the need for fully republishing jars in
 * WEB-INF/lib</strong>
 * 
 * 
 * @author Fabrizio Giustina
 * @version $Id: VirtualDirContext.java 789815 2009-06-30 16:10:09Z rjung $
 */
public class VirtualDirContext extends FileDirContext {

	/**
	 * Map containing generated virtual names for tld files under WEB-INF and
	 * the actual file reference.
	 */
	private Map<String, File> virtualMappings;

	/**
	 * Map containing a mapping for tag files that should be loaded from the
	 * META-INF dir of referenced jar files.
	 */
	private Map<String, File> tagfileMappings;

	/**
	 * <code>;</code> separated list of virtual path elements.
	 */
	private String virtualClasspath;

	/**
	 * <code>virtualClasspath</code> attribute that will be automatically set
	 * from the <code>Context</code> <code>virtualClasspath</code> attribute
	 * from the context xml file.
	 * 
	 * @param path
	 *            <code>;</code> separated list of path elements.
	 */
	public void setVirtualClasspath(String path) {
		virtualClasspath = path;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void allocate() {
		super.allocate();

		virtualMappings = new Hashtable<String, File>();
		tagfileMappings = new Hashtable<String, File>();

		// looks into any META-INF dir found in classpath entries for tld files.
		StringTokenizer tkn = new StringTokenizer(virtualClasspath, ";");
		while (tkn.hasMoreTokens()) {
			File file = new File(tkn.nextToken(), "META-INF");

			if (!file.exists() || !file.isDirectory()) {
				continue;
			}
			scanForTlds(file);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void release() {
		super.release();
		virtualMappings = null;
	}

	@Override
	public Attributes getAttributes(String name) throws NamingException {

		// handle "virtual" tlds
		if (name.startsWith("/WEB-INF/") && name.endsWith(".tld")) {
			String tldName = name.substring(name.lastIndexOf("/") + 1);
			if (virtualMappings.containsKey(tldName)) {
				return new FileResourceAttributes(virtualMappings.get(tldName));
			}
		} else if (name.startsWith("/META-INF/tags") && name.endsWith(".tag")
				|| name.endsWith(".tagx")) {

			// already loaded tag file
			if (tagfileMappings.containsKey(name)) {
				return new FileResourceAttributes(tagfileMappings.get(name));
			}

			// unknown tagfile, search for it in virtualClasspath
			StringTokenizer tkn = new StringTokenizer(virtualClasspath, ";");
			while (tkn.hasMoreTokens()) {
				File file = new File(tkn.nextToken(), name);
				if (file.exists()) {
					tagfileMappings.put(name, file);
					return new FileResourceAttributes(file);
				}
			}
		}

		return super.getAttributes(name);
	}

	@Override
	@SuppressWarnings("unchecked")
	protected ArrayList list(File file) {
		ArrayList entries = super.list(file);

		// adds virtual tlds for WEB-INF listing
		if ("WEB-INF".equals(file.getName())) {
			entries.addAll(getVirtualNamingEntries());
		}

		return entries;
	}

	@Override
	public Object lookup(String name) throws NamingException {

		// handle "virtual" tlds
		if (name.startsWith("/WEB-INF/") && name.endsWith(".tld")) {
			String tldName = name.substring(name.lastIndexOf("/") + 1);
			if (virtualMappings.containsKey(tldName)) {
				return new FileResource(virtualMappings.get(tldName));
			}
		} else if (name.startsWith("/META-INF/tags") && name.endsWith(".tag")
				|| name.endsWith(".tagx")) {

			// already loaded tag file: we are sure that getAttributes() has
			// already been called if we are here
			File tagFile = tagfileMappings.get(name);
			if (tagFile != null) {
				return new FileResource(tagFile);
			}
		}

		return super.lookup(name);
	}

	/**
	 * Scan a given dir for tld files. Any found tld will be added to the
	 * virtualMappings.
	 * 
	 * @param dir
	 *            Dir to scan for tlds
	 */
	private void scanForTlds(File dir) {

		File[] files = dir.listFiles();
		for (int j = 0; j < files.length; j++) {
			File file = files[j];

			if (file.isDirectory()) {
				scanForTlds(file);
			} else if (file.getName().endsWith(".tld")) {
				// just generate a random name using the current timestamp, name
				// doesn't matter since it needs to be referenced by URI
				String virtualTldName = "~" + System.currentTimeMillis() + "~"
						+ file.getName();
				virtualMappings.put(virtualTldName, file);
			}
		}

	}

	/**
	 * Returns a list of virtual naming entries.
	 * 
	 * @return list of naming entries, containing tlds in virtualMappings
	 */
	private List<NamingEntry> getVirtualNamingEntries() {
		List<NamingEntry> virtual = new ArrayList<NamingEntry>();

		for (String name : virtualMappings.keySet()) {

			File file = virtualMappings.get(name);
			NamingEntry entry = new NamingEntry(name, new FileResource(file),
					NamingEntry.ENTRY);
			virtual.add(entry);
		}
		return virtual;
	}

}
